---
title: All in one for Suspense
---

import Image from 'next/image'
import Link from 'next/link'
import {
  HomePage,
  Scrollycoding,
  SectionDescription,
  SectionTitle,
  TrustedBy,
} from '@/components'

<HomePage
  buttonText="Get Started"
  items={[
    {
      title: 'All Declarative APIs ready',
      desc: '`<Suspense/>`, `<ErrorBoundary/>`, `<ErrorBoundaryGroup/>`, etc. are provided. Use them easily without any efforts.',
    },
    {
      title: 'Zero peer dependency, Only React',
      desc: "It is simply extensions of react's concepts. Named friendly with originals like just `<Suspense/>`, `<ErrorBoundary/>`, `<ErrorBoundaryGroup/>`.",
    },
    {
      title: 'Suspense in SSR easily',
      desc: 'Suspensive provide clientOnly that make developer can adopt React Suspense gradually in Server-side rendering environment.',
    },
  ]}
>

<SectionTitle>Best practices to use Suspense</SectionTitle>
<br />
<SectionDescription>
  Focus on success case, not on the error, loading case.
</SectionDescription>
<br />

<Scrollycoding>
# !!steps If you write code without Suspense with TanStack Query, a representative library, you would write it like this.

In this case, you can check `isLoading` and `isError` to handle loading and error states, and remove `undefined` from data in TypeScript.

```jsx ! Page.jsx
import { useQuery } from '@tanstack/react-query'

const Page = () => {
  const userQuery = useQuery(userQueryOptions())
  const postsQuery = useQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  const promotionsQuery = useQuery(promotionsQueryOptions())

  if (
    userQuery.isLoading ||
    postsQuery.isLoading ||
    promotionsQuery.isLoading
  ) {
    return 'loading...'
  }

  if (userQuery.isError || postsQuery.isError || promotionsQuery.isError) {
    return 'error'
  }

  return (
    <Fragment>
      <UserProfile {...userQuery.data} />
      {postsQuery.data.map((post) => (
        <PostListItem key={post.id} {...post} />
      ))}
      {promotionsQuery.data.map((promotion) => (
        <Promotion key={promotion.id} {...promotion} />
      ))}
    </Fragment>
  )
}
```

# !!steps But let's assume that there are more APIs to query.

If there are more APIs to query, the code to handle the loading state and error state becomes more complicated.

```jsx ! Page.jsx
import { useQuery } from '@tanstack/react-query'

const Page = () => {
  const userQuery = useQuery(userQueryOptions())
  const postsQuery = useQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  const promotionsQuery = useQuery(promotionsQueryOptions())
  // The more this part increases, the more complicated the code becomes.

  if (
    userQuery.isLoading ||
    postsQuery.isLoading ||
    promotionsQuery.isLoading // You have to add this every time.
  ) {
    return 'loading...'
  }

  if (
    userQuery.isError ||
    postsQuery.isError ||
    promotionsQuery.isError // You have to add this every time.
  ) {
    return 'error'
  }

  return (
    <Fragment>
      <UserProfile {...userQuery.data} />
      {postsQuery.data.map((post) => (
        <PostListItem key={post.id} {...post} />
      ))}
      {promotionsQuery.data.map((promotion) => (
        <Promotion key={promotion.id} {...promotion} />
      ))}
      {/* You have to add this every time. */}
    </Fragment>
  )
}
```

# !!steps Suspense makes the code concise in terms of type. However, the depth of the component inevitably increases.

`useSuspenseQuery` can handle loading and error states externally using `Suspense` and `ErrorBoundary`.
However, since `useSuspenseQuery` is a hook, the component must be separated to place `Suspense` and `ErrorBoundary` in the parent, which causes the depth to increase.

```jsx ! Page.jsx
import { ErrorBoundary, Suspense } from '@suspensive/react'
import { useSuspenseQuery } from '@tanstack/react-query'

const Page = () => (
  <ErrorBoundary fallback="error">
    <Suspense fallback="loading...">
      <UserInfo userId={userId} />
      <PostList userId={userId} />
      <PromotionList userId={userId} />
    </Suspense>
  </ErrorBoundary>
)

const UserInfo = ({ userId }) => {
  const { data: user } = useSuspenseQuery(userQueryOptions())
  return <UserProfile {...user} />
}

const PostList = ({ userId }) => {
  const { data: posts } = useSuspenseQuery({
    ...postsQueryOptions(),
    select: (posts) => posts.filter(({ isPublic }) => isPublic),
  })
  return posts.map((post) => <PostListItem key={post.id} {...post} />)
}

const PromotionList = ({ userId }) => {
  const { data: promotions } = useSuspenseQuery(promotionsQueryOptions())
  return promotions.map((promotion) => (
    <PromotionListItem key={promotion.id} {...promotion} />
  ))
}
```

# !!steps Using Suspensive's SuspenseQuery component, you can avoid the constraints of hooks and write code more easily at the same depth.

1. Using `SuspenseQuery`, you can remove depth.

2. You remove the component called UserInfo, leaving only the presentational component like UserProfile, which makes it easier to test.

```jsx ! Page.jsx
import { ErrorBoundary, Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <ErrorBoundary fallback="error">
    <Suspense fallback="loading...">
      <SuspenseQuery {...userQueryOptions()}>
        {({ data: user }) => <UserProfile {...user} />}
      </SuspenseQuery>
      <SuspenseQuery
        {...postsQueryOptions()}
        select={(posts) => posts.filter(({ isPublic }) => isPublic)}
      >
        {({ data: posts }) =>
          posts.map((post) => <PostListItem key={post.id} {...post} />)
        }
      </SuspenseQuery>
      <SuspenseQuery
        {...promotionsQueryOptions()}
        select={(promotions) => promotions.filter(({ isPublic }) => isPublic)}
      >
        {({ data: promotions }) =>
          promotions.map((promotion) => (
            <PromotionListItem key={promotion.id} {...promotion} />
          ))
        }
      </SuspenseQuery>
    </Suspense>
  </ErrorBoundary>
)
```

</Scrollycoding>

<SectionTitle>This is why we make Suspensive</SectionTitle>
<br />
<SectionDescription>Suspense, ClientOnly, DefaultProps</SectionDescription>
<br />

<Scrollycoding>
# !!steps When using frameworks like Next.js, it can be difficult to use Suspense on the server.

Or, there are times when you don't want to use `Suspense` on the server.

```jsx ! Page.jsx
import { Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />}>
    <SuspenseQuery {...notNeedSEOQueryOptions()}>
      {({ data }) => <NotNeedSEO {...data} />}
    </SuspenseQuery>
  </Suspense>
)
```

# !!steps In this case, you can easily solve it by using Suspensive's ClientOnly.

Just wrap `ClientOnly` and it will be solved.

```jsx ! Page.jsx
import { Suspense, ClientOnly } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />}>
    <ClientOnly fallback={<InsteadOfChildrenOnlyInServer />}>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </ClientOnly>
  </Suspense>
)
```

# !!steps or Suspense in Suspense can easily handle these cases by using the clientOnly prop.

Easy, right?

```jsx ! Page.jsx
import { Suspense } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const Page = () => (
  <Suspense fallback={<Spinner />} clientOnly>
    <SuspenseQuery {...notNeedSEOQueryOptions()}>
      {({ data }) => <NotNeedSEO {...data} />}
    </SuspenseQuery>
  </Suspense>
)
```

# !!steps However, when developing, it is sometimes difficult to add fallbacks to Suspense one by one.

Especially when working on a product like Admin, there are cases where designers do not specify each one, so you want to provide default values.
In that case, try using `DefaultProps`.

```jsx ! Page.jsx
import { Suspense, DefaultProps, DefaultPropsProvider } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const defaultProps = new DefaultProps({
  Suspense: {
    fallback: <Spinner />,
  },
})

const Page = () => (
  <DefaultPropsProvider defaultProps={defaultProps}>
    <Suspense clientOnly>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </Suspense>
  </DefaultPropsProvider>
)
```

# !!steps Of course, if you want to override the default fallback, just add it.

The designer asked me to support `Skeleton` instead of the default `Spinner` in this part~! Just add it.

```jsx ! Page.jsx
import { Suspense, DefaultProps, DefaultPropsProvider } from '@suspensive/react'
import { SuspenseQuery } from '@suspensive/react-query'

const defaultProps = new DefaultProps({
  Suspense: {
    fallback: <Spinner />,
  },
})

const Page = () => (
  <DefaultPropsProvider defaultProps={defaultProps}>
    <Suspense fallback={<Skeleton />} clientOnly>
      <SuspenseQuery {...notNeedSEOQueryOptions()}>
        {({ data }) => <NotNeedSEO {...data} />}
      </SuspenseQuery>
    </Suspense>
  </DefaultPropsProvider>
)
```

</Scrollycoding>

<SectionDescription>ErrorBoundary, ErrorBoundaryGroup</SectionDescription>
<br />

<Scrollycoding>

# !!steps You should use resetKeys when you want to reset the ErrorBoundary from outside its fallback.

This has the issue of requiring `resetKey` to be passed down for deeply nested components. Additionally, you need to create a state to pass the `resetKey`.

```jsx ! Page.jsx
import { ErrorBoundary } from '@suspensive/react'

const Page = () => {
  const [resetKey, setResetKey] = useState(0)

  return (
    <Fragment>
      <button onClick={() => setResetKey((prev) => prev + 1)}>
        error reset
      </button>
      <ErrorBoundary resetKeys={[resetKey]} fallback="error">
        <ThrowErrorComponent />
      </ErrorBoundary>
      <DeepComponent resetKeys={[resetKey]} />
    </Fragment>
  )
}

const DeepComponent = ({ resetKeys }) => (
  <ErrorBoundary resetKeys={resetKeys} fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary resetKeys={resetKeys} fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps By combining the ErrorBoundary and ErrorBoundaryGroup provided by Suspensive, you can solve these issues in a very straightforward way.

Try using `ErrorBoundaryGroup`.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps However, when using ErrorBoundary, there are times when you may want to handle only specific errors.

In such cases, try using the `shouldCatch` feature provided by Suspensive's `ErrorBoundary`. By passing an Error Constructor to `shouldCatch`, you can handle only the specified errors.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary
      shouldCatch={ZodError}
      fallback="this will be render on ZodError in children"
    >
      <ThrowZodErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

# !!steps Alternatively, you can exclude that specific error from being handled.

In such cases, you can handle it by passing a callback to `shouldCatch`.

```jsx ! Page.jsx
import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'

const Page = () => (
  <ErrorBoundaryGroup>
    <ErrorBoundaryGroup.Consumer>
      {({ reset }) => <button onClick={reset}>error reset</button>}
    </ErrorBoundaryGroup.Consumer>
    <ErrorBoundary fallback="error">
      <ThrowErrorComponent />
    </ErrorBoundary>
    <DeepComponent />
  </ErrorBoundaryGroup>
)

const DeepComponent = () => (
  <ErrorBoundary fallback="error">
    <ThrowErrorComponent />
    <ErrorBoundary
      shouldCatch={(e) => !(e instanceof ZodError)}
      fallback="this will be render on Error except only ZodError in children"
    >
      <ThrowZodErrorOrErrorComponent />
    </ErrorBoundary>
  </ErrorBoundary>
)
```

</Scrollycoding>

<TrustedBy />

<SectionTitle>Thank you for contributing to Suspensive</SectionTitle>
<br />
<SectionDescription>Driven by the Community</SectionDescription>
<br />

<div
  style={{
    textAlign: 'center',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
  }}
>
  <div style={{ height: 20 }} />
  <Link
    href="https://github.com/toss/suspensive/graphs/contributors"
    target="_blank"
    style={{ textAlign: 'center' }}
  >
    <img src="https://contrib.rocks/image?repo=toss/suspensive" alt="" />
  </Link>
</div>

</HomePage>
